﻿using System;
using System.Collections.Generic;
using System.Disposables;
using System.Linq;
using System.Windows.Ria;
using SoftSource.Common;
using SoftSource.Reactive;

namespace SoftSource.Ria
{
    /// <summary>
    /// These extension wrap asynchronous RIA DomainContext methods into IObservable types.
    /// 
    /// Query wraps the load method.
    /// Execute wraps the load method, but returns Unit.
    /// WhenSubmitChanges wraps the SubmitChanges method.
    /// AsObservable wraps an invocation method.
    /// </summary>
    public static class DomainContextExtensions
    {
        #region Invoke
        /// <summary>
        /// Wraps an Invoke Operation into an IObservable that attempts to cast the return value
        /// </summary>
        /// <typeparam name="T">The value to cast to</typeparam>
        /// <param name="invoker">The function that creates the InvokeOperation</param>
        /// <returns>The IObservable that wraps the InvokeOperation</returns>
        public static IObservable<T> AsObservable<T>(Func<InvokeOperation> invoker)
        {
            return DomainContextExtensions
                .AsObservable(invoker)
                .Select(value => (T)value);
        } 

        /// <summary>
        /// Wraps an Invoke Operation into an IObservable
        /// </summary>
        /// <param name="invoker">The function that creates the InvokeOperation</param>
        /// <returns>The IObservable that wraps the InvokeOperation</returns>
        public static IObservable<object> AsObservable(Func<InvokeOperation> invoker)
        {
            return OperationBaseEx.ToObservable<InvokeOperation>(handler =>
                {
                    var invocation = invoker.TryInvoke();

                    FutureDisposable future = new FutureDisposable();
                    future.Set(Observable.FromEvent<EventHandler, EventArgs>(
                        invokeHandler => (sender, e) => invokeHandler(sender, e),
                        invokeHandler => invocation.Completed += invokeHandler,
                        invokeHandler => invocation.Completed -= invokeHandler)
                        .Do(evnt => handler(invocation))
                        .Finally(future.Dispose)
                        .Subscribe());

                    return invocation;
                }).Select(operation => operation.Value);            
        } 
        #endregion Invoke

        #region Query
        /// <summary>
        /// Queries the DomainContext via a Load, expecting a single entity to be returned
        /// </summary>
        /// <typeparam name="TEntity">The type of entity to query</typeparam>
        /// <param name="context">The DomainContext to query</param>
        /// <param name="query">The EntityQuery to execute</param>
        /// <returns>The IObservable that represents the query operation</returns>
        public static IObservable<TEntity> QuerySingleOrDefault<TEntity>(this DomainContext context, EntityQuery<TEntity> query) where TEntity : Entity
        {
            return context.Query<TEntity, TEntity>(query, collection => collection.SingleOrDefault());
        }

        /// <summary>
        /// Queries the DomainContext via a Load
        /// </summary>
        /// <typeparam name="TEntity">The type of entity to query</typeparam>
        /// <param name="context">The DomainContext to query</param>
        /// <param name="query">The EntityQuery to execute</param>
        /// <returns>The IObservable that represents the query operation</returns>
        public static IObservable<IEnumerable<TEntity>> Query<TEntity>(this DomainContext context, EntityQuery<TEntity> query) where TEntity : Entity
        {
            return context.Query<IEnumerable<TEntity>, TEntity>(query, collection => collection);
        }

        /// <summary>
        /// Queries the DomainContext via a Load
        /// </summary>
        /// <typeparam name="TEntity">The type of entity to query</typeparam>
        /// <param name="context">The DomainContext to query</param>
        /// <param name="query">The EntityQuery to execute</param>
        /// <param name="loadBehavior">The load behavior of the load query</param>
        /// <returns>The IObservable that represents the query operation</returns>
        public static IObservable<IEnumerable<TEntity>> Query<TEntity>(this DomainContext context, EntityQuery<TEntity> query, LoadBehavior loadBehavior) where TEntity : Entity
        {
            return context.Query<IEnumerable<TEntity>, TEntity>(query, collection => collection, loadBehavior);
        }

        /// <summary>
        /// Queries the DomainContext via a Load
        /// </summary>
        /// <typeparam name="TResult">The resultant type</typeparam>
        /// <typeparam name="TEntity">The type of entity to query</typeparam>
        /// <param name="context">The DomainContext to query</param>
        /// <param name="query">The EntityQuery to execute</param>
        /// <returns>The IObservable that represents the query operation</returns>
        public static IObservable<TResult> Query<TResult, TEntity>(this DomainContext context, EntityQuery<TEntity> query, Func<IEnumerable<TEntity>, TResult> selector) where TEntity : Entity
        {
            return context.Query(query, selector, LoadBehavior.RefreshCurrent);
        }     

        /// <summary>
        /// Queries the DomainContext via a Load
        /// </summary>
        /// <typeparam name="TResult">The resultant type</typeparam>
        /// <typeparam name="TEntity">The type of entity to query</typeparam>
        /// <param name="context">The DomainContext to query</param>
        /// <param name="query">The EntityQuery to execute</param>
        /// <param name="selector"></param>
        /// <param name="loadBehavior">The load behavior of the load query</param>
        /// <returns>The IObservable that represents the query operation</returns>
        public static IObservable<TResult> Query<TResult, TEntity>(this DomainContext context, EntityQuery<TEntity> query, Func<IEnumerable<TEntity>, TResult> selector, LoadBehavior loadBehavior) where TEntity : Entity
        {
            return OperationBaseEx.ToObservable<LoadOperation<TEntity>>(handler => context.Load(query, loadBehavior, handler, null))
                .Select(lo => selector(lo.Entities));            
        }
        #endregion

        #region Execute
        /// <summary>
        /// Executes a load query against the DomainContext into the local cache
        /// </summary>
        /// <typeparam name="TEntity">The type of entity to query</typeparam>
        /// <param name="context">The DomainContext to query</param>
        /// <param name="query">The EntityQuery to execute</param>
        /// <returns>The IObservable that represents the query operation</returns>
        public static IObservable<Unit> Execute<TEntity>(this DomainContext context, EntityQuery<TEntity> query) where TEntity : Entity
        {
            return context.Execute(query, LoadBehavior.RefreshCurrent);
        }

        /// <summary>
        /// Executes a load query against the DomainContext into the local cache
        /// </summary>
        /// <typeparam name="TEntity">The type of entity to query</typeparam>
        /// <param name="context">The DomainContext to query</param>
        /// <param name="query">The EntityQuery to execute</param>
        /// <param name="loadBehavior">The load behavior of the load query</param>
        /// <returns>The IObservable that represents the query operation</returns>
        public static IObservable<Unit> Execute<TEntity>(this DomainContext context, EntityQuery<TEntity> query, LoadBehavior loadBehavior) where TEntity : Entity
        {
            return context.Query(query, entities => new Unit(), loadBehavior);
        } 
        #endregion

        #region SubmitChanges
        /// <summary>
        /// Wraps the SubmitOperation into an IObservable
        /// </summary>
        /// <param name="context">The DomainContext to submit the changes to</param>
        /// <returns>The IObservable that wraps the SubmitChanges operation</returns>
        public static IObservable<SubmitOperation> SaveChanges(this DomainContext context)
        {
            return OperationBaseEx.ToObservable<SubmitOperation>(handler => context.SubmitChanges(handler, null));
        }

        public static IObservable<SubmitOperation> SerialSaveChanges(this DomainContext context)
        {
            return context
                .SaveChanges()
                .QueueSubscribers();
        }
        #endregion SubmitChanges
    }
}
